﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Collections;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Globalization;

namespace StackExchangeApi
{
    /// <summary>
    /// Used to mediate HTTP traffic.
    /// 
    /// We need a common point to prevent flooding any
    /// trilogy site with ridiculous number of requests and
    /// thereby getting 403'd.
    /// </summary>
    internal static class ConnectionMediator
    {
        #if DEBUG
        internal static long TOTAL_REQUEST_COUNT;

        internal static long REQUEST_COUNT;
        #endif

        /// <summary>
        /// The "Trilogy Sites" can respond in two different ways,
        /// either as JSON (horray, a proper API) or
        /// as HTML (boo, screenscraping).
        /// 
        /// We need to keep track of which is which for throttling purposes.
        /// </summary>
        private enum ResponseType{
            JSON,
            HTML
        }

        public static readonly int REASONABLE_JSON_LIMIT = 5;
        public static readonly int REASONABLE_HTML_LIMIT = 1;

        private static volatile String user_agent;

        /// <summary>
        /// The UserAgent string to be sent along with every
        /// request.
        /// 
        /// Note that setting this variable results in formatting,
        /// so UserAgent = UserAgent is not a NOP.
        /// </summary>
        internal static String UserAgent
        {
            get { return user_agent; }
            set
            {
                user_agent =
                    String.Format(
                    CultureInfo.InvariantCulture,
                    "{0} [{1}]",
                    value,
                    "using SXAPI: http://code.google.com/p/stackexchange-api/");
            }
        }

        #if DEBUG
        /// <summary>
        /// For debugging purposes.
        /// 
        /// Specifies the FASTEST requests will be made, 
        /// over all traffic types.
        /// </summary>
        public static int REASONABLE_LIMIT = Math.Max(REASONABLE_HTML_LIMIT, REASONABLE_JSON_LIMIT);
        #endif

        private static TimeSpan ONE_SECOND = new TimeSpan(0, 0, 1);

        private static WebClient common_client = new WebClient();

        private static TimeSpan WAIT_BETWEEN_JSON_REQUESTS = new TimeSpan(ONE_SECOND.Ticks / REASONABLE_JSON_LIMIT);
        private static TimeSpan WAIT_BETWEEN_HTML_REQUESTS = new TimeSpan(ONE_SECOND.Ticks / REASONABLE_HTML_LIMIT);
        private static Dictionary<StackExchangeSite, DateTime> LAST_REQUESTS = new Dictionary<StackExchangeSite, DateTime>()
        {
            { StackExchangeSite.Meta, DateTime.MinValue },
            { StackExchangeSite.ServerFault, DateTime.MinValue },
            { StackExchangeSite.StackOverflow, DateTime.MinValue },
            { StackExchangeSite.SuperUser, DateTime.MinValue }
        };

        private static Dictionary<StackExchangeSite, ResponseType> LAST_RESPONSE_TYPE = new Dictionary<StackExchangeSite, ResponseType>()
        {
            { StackExchangeSite.Meta, ResponseType.JSON },
            { StackExchangeSite.ServerFault, ResponseType.JSON },
            { StackExchangeSite.StackOverflow, ResponseType.JSON },
            { StackExchangeSite.SuperUser, ResponseType.JSON }
        };

        #if DEBUG

        private static DateTime start_time = DateTime.MinValue;

        /// <summary>
        /// For debug/test purposes only
        /// </summary>
        internal static void StartTimer()
        {
            start_time = DateTime.Now;
        }

        /// <summary>
        /// Time since StartTimer() was called
        /// </summary>
        internal static TimeSpan ElapsedTime
        {
            get { return DateTime.Now - start_time; }
        }

        /// <summary>
        /// For debug/test purposes only
        /// </summary>
        internal static void ResetRequestCount()
        {
            Interlocked.Exchange(ref REQUEST_COUNT, 0);
        }

        #endif

        /// <summary>
        /// Method to make requests on a trilogy site.
        /// 
        /// This limits the number of requests to REASONABLE_LIMIT/FIVE_SECONDS.
        /// </summary>
        /// <param name="uri">To request</param>
        /// <returns>The result call</returns>
        public static String MakeRequest(Uri uri)
        {
            if (UserAgent == null || !SXAPI.Initialized)
                throw new NotInitializedException();

            TimeSpan diff;
            StackExchangeSite site = Utils.GetSiteForAddress(uri.Host);
            TimeSpan wait;

            lock (LAST_REQUESTS)
            {
                diff = DateTime.Now - LAST_REQUESTS[site];
                switch (LAST_RESPONSE_TYPE[site])
                {
                    case ResponseType.HTML: wait = WAIT_BETWEEN_HTML_REQUESTS; break;
                    case ResponseType.JSON: wait = WAIT_BETWEEN_JSON_REQUESTS; break;
                    default: throw new TrilogySiteDownloadException("Excepted HTML or JSON");
                }
            }

            while (diff < wait)
            {
                Thread.Sleep(diff);

                lock (LAST_REQUESTS)
                {
                    diff = DateTime.Now - LAST_REQUESTS[site];
                    switch (LAST_RESPONSE_TYPE[site])
                    {
                        case ResponseType.HTML: wait = WAIT_BETWEEN_HTML_REQUESTS; break;
                        case ResponseType.JSON: wait = WAIT_BETWEEN_JSON_REQUESTS; break;
                    }
                }
            }

            String ret;

            try
            {
                lock (LAST_REQUESTS)
                {
                    common_client.Headers[HttpRequestHeader.AcceptEncoding] = "gzip,deflate";
                    common_client.Headers[HttpRequestHeader.UserAgent] = UserAgent;
                    byte[] downloadData = common_client.DownloadData(uri);

                    StreamReader reader;

                    String contentEncoding = common_client.ResponseHeaders[HttpResponseHeader.ContentEncoding];
                    String contentType = common_client.ResponseHeaders["Content-Type"];

                    if (!String.IsNullOrEmpty(contentEncoding))
                    {
                        if (contentEncoding.Contains("gzip"))
                        {
                            GZipStream decompress = new GZipStream(new MemoryStream(downloadData), CompressionMode.Decompress);
                            reader = new StreamReader(decompress);
                        }
                        else
                        {
                            if (contentEncoding.Contains("deflate"))
                            {
                                DeflateStream decompress = new DeflateStream(new MemoryStream(downloadData), CompressionMode.Decompress);
                                reader = new StreamReader(decompress);
                            }
                            else
                            {
                                #if DEBUG
                                //This isn't necessarily a bug... but is concerning
                                //  as (in theory) all S[O,F,U] traffic is compressed.

                                //TODO:  This code needs to be gone sometime, the number
                                //       of #if macros is becoming concerning
                                String message =
                                    String.Format(
                                    CultureInfo.InvariantCulture,
                                    "[{0}] did not response with a known Content-Encoding (instead [{1}]), and was therefore not compressed.",
                                    uri,
                                    contentEncoding);

                                Debug.WriteLine(message);
                                #endif

                                reader = new StreamReader(new MemoryStream(downloadData));
                            }
                        }
                    }
                    else
                    {
                        #if DEBUG
                        //This isn't necessarily a bug... but is concerning
                        //  as (in theory) all S[O,F,U] traffic is compressed.

                        //TODO:  This code needs to be gone sometime, the number
                        //       of #if macros is becoming concerning
                        String message =
                            String.Format(
                            CultureInfo.InvariantCulture,
                            "[{0}] did not response with a known Content-Encoding (instead [{1}]), and was therefore not compressed.",
                            uri,
                            contentEncoding);

                        Debug.WriteLine(message);
                        #endif

                        reader = new StreamReader(new MemoryStream(downloadData));
                    }

                    ret = reader.ReadToEnd();
                    LAST_REQUESTS[site] = DateTime.Now;

                    if (!String.IsNullOrEmpty(contentType) && contentType.Contains("json"))
                    {
                        LAST_RESPONSE_TYPE[site] = ResponseType.JSON;
                    }
                    else
                    {
                        LAST_RESPONSE_TYPE[site] = ResponseType.HTML;
                    }
                }
            }
            catch(Exception e)
            {
                throw new TrilogySiteDownloadException(uri, e);
            }

            #if DEBUG
            //For testing/debug purposes
            Interlocked.Increment(ref REQUEST_COUNT);
            Interlocked.Increment(ref TOTAL_REQUEST_COUNT);
            #endif

            return ret;
        }
    }
}
